using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics.PackedVector;

using DataTypes;

namespace ProjectCastamere
{
    /// <summary>
    /// The glue that holds a particle system together. 
    /// Parses the XML file etc and sets up the stuff to draw and update correctly etc
    /// </summary>
    public class ParticleSystem
    {
        // The name of the XML file describing this particle system
        string settingsName;

        // the settings class controls the appearance and animations etc of a particle system
        ParticleSettings settings;

        // content manager to load stuff
        ContentManager content;
        GraphicsDevice device;

        // The custom shader effect for drawing particles
        // This is done entirely on the GPU. No CPU work required
        Effect particleEffect;

        // Some shortcurts for accessing params that change quite regular
        EffectParameter effectViewParameter;
        EffectParameter effectProjectionParameter;
        EffectParameter effectViewportScaleParameter;
        EffectParameter effectTimeParameter;

        // An array of particles
        ParticleVertex[] particles;

        // A VBO holding our particles. This contains the same data as thee above array
        // but its copied to this to allow the GPU to access it 
        DynamicVertexBuffer vertexBuffer;

        // The index buffer turns each set of four vertices into a Particle Quad(two triangles)
        IndexBuffer indexBuffer;

        // Some particle management stuff
        int firstActiveParticle;
        int firstNewParticle;
        int firstFreeParticle;
        int firstRetiredParticle;

        // Store the current time in seconds
        float currentTime;

        // count how many times draw has been called. This is useful to note since it allows us to manage when to 
        // safely bring the retired particles back into the free list
        int drawCounter;

        // Random number generator
        static Random random = new Random();

        // Constructor
        public ParticleSystem(Application app, string settingsName)
        {
            this.device = app.GraphicsDevice;
            this.content = app.Content;
            this.settingsName = settingsName;

            // Load the content
            LoadContent();
        }

        /// <summary>
        /// Load the particle settings content and init the VBO buffers etc so we can ship stuff to the GPU
        /// </summary>
        private void LoadContent()
        {
            // Load the settings file
            settings = content.Load<ParticleSettings>(settingsName);

            // allocate the particle array, fill in the corner fields which never change
            particles = new ParticleVertex[settings.MaxParticles * 4];

            for (int i = 0; i < settings.MaxParticles; i++)
            {
                particles[i * 4 + 0].Corner = new Short2(-1,-1);
                particles[i * 4 + 1].Corner = new Short2(1, -1);
                particles[i * 4 + 2].Corner = new Short2(1, 1);
                particles[i * 4 + 3].Corner = new Short2(-1, 1);
            }

            // Load the Particle Effect
            LoadParticleEffect();

            // Create a new dynamic VBO
            vertexBuffer = new DynamicVertexBuffer(device, ParticleVertex.VertexDeclaration, settings.MaxParticles * 4, BufferUsage.WriteOnly);

            // Create and populate the index buffer
            ushort[] indices = new ushort[settings.MaxParticles * 6];

            for (int i = 0; i < settings.MaxParticles; i++)
            {
                indices[i * 6 + 0] = (ushort)(i * 4 + 0);
                indices[i * 6 + 1] = (ushort)(i * 4 + 1);
                indices[i * 6 + 2] = (ushort)(i * 4 + 2);

                indices[i * 6 + 3] = (ushort)(i * 4 + 0);
                indices[i * 6 + 4] = (ushort)(i * 4 + 2);
                indices[i * 6 + 5] = (ushort)(i * 4 + 3);
            }

            // Set up the index buffer
            indexBuffer = new IndexBuffer(device, typeof(ushort), indices.Length, BufferUsage.WriteOnly);

            indexBuffer.SetData(indices);
        }

        // Return the number of particles, wont really need this for anything
        public int GetNumberOfParticles()
        {
            return settings.MaxParticles;
        }

        /// <summary>
        /// Small helper class that helps to load and init particle effects
        /// </summary>
        void LoadParticleEffect()
        {
            // Load the effect shader
            Effect effect = content.Load<Effect>("Particles/shader/ParticleShader");

            // If we were to have several particle systems, the content manager will return a single
            // shadered effect instance to them all. 
            // To prevent each particle system overriding another particle systems settings, we can clone the effect
            // to stop this happening
            particleEffect = effect.Clone();

            // Set up the params for the effect
            EffectParameterCollection parameters = particleEffect.Parameters;

            // Look up the shortcuts for params that change each frame
            effectViewParameter = parameters["View"];
            effectProjectionParameter = parameters["Projection"];
            effectViewportScaleParameter = parameters["ViewportScale"];
            effectTimeParameter = parameters["CurrentTime"];

            // set the constant values, dependant on a particular particle system
            // that dont change
            parameters["Duration"].SetValue((float)settings.Duration.TotalSeconds);
            parameters["DurationRandomness"].SetValue(settings.DurationRandomness);
            parameters["Gravity"].SetValue(settings.Gravity);
            parameters["EndVelocity"].SetValue(settings.EndVelocity);
            parameters["MinColor"].SetValue(settings.MinColor.ToVector4());
            parameters["MaxColor"].SetValue(settings.MaxColor.ToVector4());

            parameters["RotateSpeed"].SetValue(
                new Vector2(settings.MinRotateSpeed, settings.MaxRotateSpeed));

            parameters["StartSize"].SetValue(
                new Vector2(settings.MinStartSize, settings.MaxStartSize));

            parameters["EndSize"].SetValue(
                new Vector2(settings.MinEndSize, settings.MaxEndSize));

            // Load the particle texture, and set it onto the effect.
            Texture2D texture = content.Load<Texture2D>(settings.TextureName);

            parameters["Texture"].SetValue(texture);
        }

        /// <summary>
        /// Simply update the particle system
        /// </summary>
        /// <param name="gameTime"></param>
        public void Update(GameTime gameTime)
        {
            if (gameTime == null)
                throw new ArgumentNullException("gameTime");

            // set the current time
            currentTime += (float)gameTime.ElapsedGameTime.TotalSeconds;

            // Do some particle management
            RetireParticles();
            FreeParticles();

            // To prevent floating point precision issues
            // Reset the time value when no particles are being drawn
            if (firstActiveParticle == firstFreeParticle)
                currentTime = 0;

            if (firstRetiredParticle == firstActiveParticle)
                drawCounter = 0;
        }

        /// <summary>
        /// Helper class for checking when active particles have reached the end of their life
        /// It moves old particles from the active area of the queue to the retired area(note they will not collect a pension) :P 
        /// </summary>
        void RetireParticles()
        {
            // find out the particles life
            float particleDuration = (float)settings.Duration.TotalSeconds;

            while (firstActiveParticle != firstNewParticle)
            {
                // Is this particle old enough to retire?
                // Multiple the active particle index by four, because each
                // particle consists of a quad that is made of four vertices
                float particleAge = currentTime-particles[firstActiveParticle * 4].Time;

                if (particleAge < particleDuration)
                    break;

                // Remember the time at which we retired this particle
                particles[firstActiveParticle * 4].Time = drawCounter;

                // Move the particle from the active to the retired queue
                firstActiveParticle++;

                if (firstActiveParticle >= settings.MaxParticles)
                    firstActiveParticle = 0;
            }
        }

        /// <summary>
        /// Helper class for checking when retired particles have been kept around long enough to ensure the GPU is no longer using them
        /// Moves them back into the active queue
        /// </summary>
        void FreeParticles()
        {
            while (firstRetiredParticle != firstActiveParticle)
            {
                // Has this particle been unused long enough that
                // the GPU is sure to be finished with it?
                // We multiply the retired particle index by four, because each
                // particle consists of a quad that is made up of four vertices.
                int age = drawCounter - (int)particles[firstRetiredParticle * 4].Time;

                // The GPU is never supposed to get more than 2 frames behind the CPU.
                // We add 1 to that, just to be safe in case of buggy drivers that
                // might bend the rules and let the GPU get further behind.
                if (age < 3)
                    break;

                // Move the particle from the retired to the free queue.
                firstRetiredParticle++;

                if (firstRetiredParticle >= settings.MaxParticles)
                    firstRetiredParticle = 0;
            }
        }

        // Finally draw the particles
        public void Draw(GameTime gameTime)
        {
            // restore the VBO contents if the graphics device was lost
            if (vertexBuffer.IsContentLost)
            {
                vertexBuffer.SetData(particles);
            }

            // if there are any new particles waiting in the newly added queue
            // upload them to the GPU for drawing
            if (firstNewParticle != firstFreeParticle)
            {
                AddNewParticlesToVertexBuffer();
            }

            // If any active particles, draw them
            if (firstActiveParticle != firstFreeParticle)
            {
                device.BlendState = settings.BlendState;
                device.DepthStencilState = DepthStencilState.DepthRead;

                // Set an effect parameter describing the viewport size. This is
                // needed to convert particle sizes into screen space point sizes.
                effectViewportScaleParameter.SetValue(new Vector2(0.5f / device.Viewport.AspectRatio, -0.5f));

                // Set an effect parameter describing the current time. All the vertex
                // shader particle animation is keyed off this value.
                effectTimeParameter.SetValue(currentTime);

                // Set the particle vertex and index buffer.
                device.SetVertexBuffer(vertexBuffer);
                device.Indices = indexBuffer;

                // Activate the particle effect.
                foreach (EffectPass pass in particleEffect.CurrentTechnique.Passes)
                {
                    pass.Apply();

                    if (firstActiveParticle < firstFreeParticle)
                    {
                        // If the active particles are all in one consecutive range,
                        // we can draw them all in a single call.
                        device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0,
                                                     firstActiveParticle * 4, (firstFreeParticle - firstActiveParticle) * 4,
                                                     firstActiveParticle * 6, (firstFreeParticle - firstActiveParticle) * 2);
                    }
                    else
                    {
                        // If the active particle range wraps past the end of the queue
                        // back to the start, we must split them over two draw calls.
                        device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0,
                                                     firstActiveParticle * 4, (settings.MaxParticles - firstActiveParticle) * 4,
                                                     firstActiveParticle * 6, (settings.MaxParticles - firstActiveParticle) * 2);

                        if (firstFreeParticle > 0)
                        {
                            device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0,
                                                         0, firstFreeParticle * 4,
                                                         0, firstFreeParticle * 2);
                        }
                    }
                }

                // Reset some of the renderstates that we changed,
                // so as not to mess up any other subsequent drawing.
                device.DepthStencilState = DepthStencilState.Default;
            }
            drawCounter++;
        }

        /// <summary>
        /// Another helper class, that is used to upload new particles
        /// from the array to the GPU vertex buffer
        /// </summary>
        void AddNewParticlesToVertexBuffer()
        {
            // retrieve the stride
            int stride = ParticleVertex.SizeInBytes;

            if (firstNewParticle < firstFreeParticle)
            {
                // If the new particles are all in one consecutive range,
                // we can upload them easily in one single call
                vertexBuffer.SetData(firstNewParticle * stride * 4, particles,
                                    firstNewParticle * 4,
                                    (firstFreeParticle - firstNewParticle) * 4,
                                    stride, SetDataOptions.NoOverwrite);
            }
            else
            {
                // If the new particle range wraps past the end of the queue
                // back to the start, we must split them over two upload calls.
                vertexBuffer.SetData(firstNewParticle * stride * 4, particles,
                                     firstNewParticle * 4,
                                     (settings.MaxParticles - firstNewParticle) * 4,
                                     stride, SetDataOptions.NoOverwrite);

                if (firstFreeParticle > 0)
                {
                    vertexBuffer.SetData(0, particles,
                                         0, firstFreeParticle * 4,
                                         stride, SetDataOptions.NoOverwrite);
                }
            }

            // move the particles we just uploaded from the new to the active queue
            firstNewParticle = firstFreeParticle;
        }

        // Set methods
        // Sets the camera view and projection matrices to draw this system
        public void SetCamera(Matrix view, Matrix projection)
        {
            effectViewParameter.SetValue(view);
            effectProjectionParameter.SetValue(projection);
        }

        /// <summary>
        /// Adds a new particle to the system
        /// </summary>
        /// <param name="position"></param>
        /// <param name="velocity"></param>
        public void AddParticle(Vector3 position, Vector3 velocity)
        {
            // Figure out where in the circular queue to allocate the new particle.
            int nextFreeParticle = firstFreeParticle + 1;

            if (nextFreeParticle >= settings.MaxParticles)
                nextFreeParticle = 0;

            // If there are no free particles, we just have to give up.
            if (nextFreeParticle == firstRetiredParticle)
                return;

            // Adjust the input velocity based on how much
            // this particle system wants to be affected by it.
            velocity *= settings.EmitterVelocitySensitivity;

            // Add in some random amount of horizontal velocity.
            float horizontalVelocity = MathHelper.Lerp(settings.MinHorizontalVelocity,
                                                       settings.MaxHorizontalVelocity,
                                                       (float)random.NextDouble());

            double horizontalAngle = random.NextDouble() * MathHelper.TwoPi;

            velocity.X += horizontalVelocity * (float)Math.Cos(horizontalAngle);
            velocity.Z += horizontalVelocity * (float)Math.Sin(horizontalAngle);

            // Add in some random amount of vertical velocity.
            velocity.Y += MathHelper.Lerp(settings.MinVerticalVelocity,
                                          settings.MaxVerticalVelocity,
                                          (float)random.NextDouble());

            // Choose four random control values. These will be used by the vertex
            // shader to give each particle a different size, rotation, and color.
            Color randomValues = new Color((byte)random.Next(255),
                                           (byte)random.Next(255),
                                           (byte)random.Next(255),
                                           (byte)random.Next(255));

            // Fill in the particle vertex structure.
            for (int i = 0; i < 4; i++)
            {
                particles[firstFreeParticle * 4 + i].Position = position;
                particles[firstFreeParticle * 4 + i].Velocity = velocity;
                particles[firstFreeParticle * 4 + i].Random = randomValues;
                particles[firstFreeParticle * 4 + i].Time = currentTime;
            }

            firstFreeParticle = nextFreeParticle;
        }
    }
}
